Mini Project 2 - ece481-01

Tyler Rarick

In [1]:
import IPython.display as ipd # For in-notebook audio
import numpy as np           # Nice data structures and math operations
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
In [2]:
fs = 44100   # Sampling rate
duration = 2 # Duration
fref = 440   # Reference frequency
note = 3     # C4
In [3]:
class Song:
    
    def __init__(self, fs=44100, fref=440, bpm=120):
        self.fs = fs
        self.fref= fref
        self.bpm = bpm
        self.df = pd.DataFrame(columns=['start', 'note',
                                        'duration', 'intensity'])  
        return
        
    def __BarToSamples(self):
        return int(self.__BarToSeconds()*self.fs)
    
    def __BarToSeconds(self):
        return 4*self.__BeatToSeconds()
    
    def __BeatToSeconds(self):
        return (1/self.bpm)*60
    
    # Duration and start in bars
    def AddNote(self, start, note, duration=0.25, intensity=0.1):
        newNote = pd.DataFrame([[start, note, duration, intensity]],
                                columns=['start', 'note',
                                         'duration', 'intensity'])
        
        self.df = self.df.append(newNote)
        return
                
    def MajorToNote(self):
        return
    
    def Compile(self, instrument):    
        df = self.df.copy()
        barSamples = self.__BarToSamples()
        arraySize = (df['start'] + df['duration']/4).max()*barSamples
        arraySize = np.ceil(arraySize).astype(np.int)
        
        x = np.zeros(arraySize)
        
        df['note'] = df['note'].apply(Song.__NoteToFreq, args=(self.fref,))
        df['start'] = df['start']*barSamples
        df['duration'] = df['duration']*self.__BeatToSeconds()
        
        for i, note in df.iterrows():
            subArray = instrument.play(note['note'], 
                                       self.fs,
                                       note['duration'],
                                       note['intensity'])
            start = np.ceil(note['start']).astype(np.int)
            end = start + subArray.size
            
            x[start:end] += subArray
            
        return 0.5*x/max(x)
    
    def __NoteToFreq(note, fref):
        return fref*np.power(2, note/12)
In [4]:
class Instrument:
    
    def __init__(self):
        if self.__class__ == Instrument:
            raise NotImplementedError
        
    # Duration in seconds
    def play(self, f, fs, duration, intensity):
        if self.__class__ == Instrument:
            raise NotImplementedError
            
    def EnvelopeView(subClass):
        return subClass.play(1, 100, 1, 1)
In [5]:
class SineWave(Instrument):
    
    # Duration in seconds
    def play(f, fs, duration, intensity):
        samples = fs*duration
        t = np.linspace(0,duration, samples)

        w = 2*np.pi*f
        
        return intensity*np.sin(w*t)
    
    def EnvelopeView():
        return Instrument.EnvelopeView(SineWave)
In [6]:
class SawTooth(Instrument):
    
    # Duration in seconds
    def play(f, fs, duration, intensity):
        samples = fs*duration
        t = np.linspace(0,duration, samples)
        
        T = 1/f
        
        # t mod T represents progress to T
        # Amplitude scaled by /T*intensity
        return intensity*(t % T)/T
    
    def EnvelopeView():
        return Instrument.EnvelopeView(SawTooth)
In [7]:
class SquareWave(Instrument):
    
    # Duration in seconds
    def play(f, fs, duration, intensity):
        samples = fs*duration
        t = np.linspace(0,duration, samples)
        
        T = 1/f
        
        # Same as Sawtooth but rounded to integer 0/1
        return intensity*np.round((t % T)/T)
    
    def EnvelopeView():
        return Instrument.EnvelopeView(SquareWave)
In [8]:
class TriangleWave(Instrument):
    
    # Duration in seconds
    def play(f, fs, duration, intensity):
        samples = fs*duration
        t = np.linspace(0,duration, samples)
        
        T = 1/f
        
        # 2*abs(square wave - sawtooth)
        return 2*intensity*abs(np.round((t % T)/T) - (t % T)/T)
    
    def EnvelopeView():
        return Instrument.EnvelopeView(TriangleWave)
In [9]:
class Snare(Instrument):
    def play(f, fs, duration, intensity):
        return np.random.rand(np.ceil(fs*duration).astype(np.int))
    
    def EnvelopeView():
        return Instrument.EnvelopeView(Snare)
In [10]:
song = Song(fs=44100, fref=440, bpm=120)
song.AddNote(0, 0, duration=8)
song.AddNote(0.25, 2, duration=1)
song.AddNote(0.5, 4, duration=6)
song.AddNote(0.75, 5, duration=1)
song.AddNote(1, 7, duration=4)
song.AddNote(1.25, 9, duration=1)
song.AddNote(1.5, 11, duration=1)
song.AddNote(1.75, 12, duration=1)

song.AddNote(2, 12-12, duration=1)
song.AddNote(2.25, 11-12, duration=1)
song.AddNote(2.5, 9-12, duration=1)
song.AddNote(2.75, 7-12, duration=13)
song.AddNote(3, 5-12, duration=1)
song.AddNote(3.25, 4-12, duration=11)
song.AddNote(3.5, 2-12, duration=1)
song.AddNote(3.75, 0-12, duration=9)
song.AddNote(3.75, 0-24, duration=9)
In [11]:
x = song.Compile(instrument=SineWave)
plt.plot(SineWave.EnvelopeView())
ipd.Audio(x, rate=fs)
Out[11]:
In [12]:
x = song.Compile(instrument=SawTooth)
plt.plot(SawTooth.EnvelopeView())
ipd.Audio(x, rate=fs)
Out[12]:
In [13]:
x = song.Compile(instrument=SquareWave)
plt.plot(SquareWave.EnvelopeView())
ipd.Audio(x, rate=fs)
Out[13]:
In [14]:
x = song.Compile(instrument=TriangleWave)
plt.plot(TriangleWave.EnvelopeView())
ipd.Audio(x, rate=fs)
Out[14]:
In [15]:
drumSong = Song(fs=44100, fref=440, bpm=120)
for i in range(16):    
    drumSong.AddNote(i*0.25, 1, duration=0.05)
In [16]:
d = drumSong.Compile(instrument=Snare)
plt.plot(Snare.EnvelopeView())
ipd.Audio(d, rate=fs)
Out[16]: